/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

//=Container

(function () {

/**
 * Removes a destroyed item from the slot.
 *
 * @private
 */
function itemDestroy() {
  this.destroing = true;
  this.owner.destroing || this.owner.remove(this);
}

/**
* Renters a component into a slot.
*
* @param {Object|String|Container} item A component configuration object.
* @param {number} i Item index.
* @param {Object[]|String[]|Container[]} items The collection of items.
* @private
*/
function createItem(item, i, items) {
  this.alias[item] && (items[i] = item = this.alias[item]);
  _.isString(item) && (items[i] = item = {type: item});
  item.renderTo = this.el;
  item.owner = this;
  if (Class.is('Container', item)) {
    item.el ? this.el.append(item.el) : item.show();
  } else {
    (function apply(item, defaults) {
      _.each(defaults, function (value, i) {
        if (_.isPlainObject(value) && (_.isUndefined(item[i]) ||
            _.isPlainObject(item[i]))) {
          _.isUndefined(item[i]) && (item[i] = {});
          apply(item[i], value);
        } else if (_.isUndefined(item[i])) {
          item[i] = value;
        }
      });
    }(item, this.defaults));
    items[i] = item = Class.create(item).show();
  }
  item.on('destroy', itemDestroy);
}

/**
 * Finds the nearest rendered item and renders itself beside it.
 *
 * @param {Number} i
 * @param {Function} callback
 */
function renderAt(i, callback) {
  var distance = this.items.length, curr, prepend;
  this.each(function (item, index) {
    if (i !== index && item.el) {
      var dist = Math.abs(index - i);
      if (dist < distance) {
        distance = dist;
        curr = item;
        prepend = i < index;
      }
    }
  });
  callback.call(this, i);
  curr ? (prepend ? curr.el.before(this.el) : curr.el.after(this.el)) :
    this.renderTo[this.prependMode ? 'prepend' : 'append'](this.el);
}

/**
 * Activates a slot item.
 *
 * @param {number} i
 * @private
 */
function activate(i) {
  if (this.el) {
    if (null === i) {
      this.each(createItem);
      this.renderTo[this.prependMode ? 'prepend' : 'append'](this.el);
    } else {
      this.each(function (item, index) { (i !== index) && item.hide &&
        item.hide() });
      if (this.items[i].el) {
        this.items[i].show();
      } else {
        renderAt.call(this, i, function () {
          createItem.call(this, this.items[i], i, this.items);
        });
      }
    }
  }
}

/**
 * Updates the length property, a pointer to the first and the last item.
 *
 * @private
 */
function updatePointers() {
  this.length = _.reject(this.items, function (item) {
    return _.isUndefined(item);
  }).length;
  this.each(function (item) {
    this.first = item;
    return false;
  });
  var i = this.items.length;
  while (i--) {
    if (!_.isUndefined(this.items[i])) {
      this.last = this.items[i];
      break;
    }
  }
  this.fire('change');
}

/**
 * A slot that holds multiple containers.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 * @class {Slot}
 * @extends {Container}
 */
Class.define('Slot', {

  extend: 'Container',

  activeItem: null,

  /** @constructs */
  init: function () {
    _super();
    this.defaults = this.defaults || {};
    this.alias = this.alias || {};
    this.items = this.items || [];
    _.isArray(this.items) || (this.items = [this.items]);
    updatePointers.call(this);
  },

  /**
   * Iterates over slot items.
   *
   * @param {Function} fn Callback.
   * @param {Object} [context] Context.
   * @returns {array}
   */
  each: function (fn, context) {
    return _.each(this.items, function (item) {
      if (!_.isUndefined(item)) return fn.apply(this, arguments);
    }, context || this);
  },

  /**
   * Retrieves a slot item at the specified index.
   *
   * @param {number} i
   * @returns {Container|Object|String}
   */
  get: function (i) {
    return this.items[i];
  },

  /**
   * Returns the index at which a particular item is stored in the slot of -1 if
   * the item is not found.
   *
   * @param {Object|String|Container} item
   * @returns {Number}
   */
  indexOf: function (item) {
    return _.indexOf(this.items, item);
  },

  /** @inheritDoc */
  createElement: function () {
    return $(document.createDocumentFragment());
  },

  /** @inheritDoc */
  show: function () {
    if (this.el) {
      this.each(function (item) { item.show && item.show() });
      this.fire('show');
    } else {
      _super();
    }
    return this;
  },

  /** @inheritDoc */
  hide: function () {
    this.el && this.each(function (item) { item.hide && item.hide() });
    this.fire('hide');
    return this;
  },

  /**
   * Adds new items to a slot.
   * Returns inserted items.
   *
   * @param {Object|Object[]|Container|Container[]} items
   * @param {number} [i] Relative index, don't rely on it always staying the
   *                     same, especially if you try to insert something into an
   *                     already occupied index.
   * @returns {Object[]|String[]|Container[]}
   */
  insert: function (items, i) {
    if (!items) return [];
    var isArray = _.isArray(items);
    isArray || (items = [items]);
    if (!items.length) return [];
    _.each(items, function (item) {
      var i = _.indexOf(this.items, item);
      (-1 === i) || (this.items[i] = undefined);
    }, this);
    if (_.isUndefined(i)) i = this.items.length;
    var len = i + items.length, arr = [], l, start = i;
    (this.items.length <= len) && (this.items.length = len);
    for (l = 0; i < len; i++) {
      _.isUndefined(this.items[i]) || arr.push(this.items[i]);
      this.items[i] = items[l++];
    }
    for (l = 0; l < arr.length; l++) {
      if (this.items.length <= len) {
        this.items.push(arr[l]);
      } else {
        _.isUndefined(this.items[len]) || arr.push(this.items[len]);
        this.items[len] = arr[l];
      }
      len++;
    }
    if (this.el) {
      if (null === this.activeItem) {
        renderAt.call(this, start, function (start) {
          _.each(items, function (item, i, items) {
            createItem.apply(this, arguments);
            this.items[start++] = items[i];
          }, this);
        });
      } else if ((start >= this.activeItem) &&
          (start < (this.activeItem + items.length))) {
        activate.call(this, this.activeItem);
      }
    }
    updatePointers.call(this);
    var range = this.items.slice(start, items.length + start);
    return (isArray || !range.length) ? range : range[0];
  },

  /**
   * Removes items from the slot by either a reference or an index.
   * Returns removed items that were not destroyed.
   *
   * @returns {Array}
   */
  remove: function () {
    var resp = [];
    _.each(arguments, function (item) {
      var i = _.isNumber(item) ? item : _.indexOf(this.items, item);
      item = this.items[i];
      if (item) {
        this.items[i] = undefined;
        if (this.el && item.autoDestroy && item.destroy) {
          item.destroing || item.destroy();
          return;
        }
        resp.push(item);
      }
    }, this);
    updatePointers.call(this);
    return resp;
  },

  /**
   * Sets the currently visible item in a slot.
   *
   * @param {number} i
   * @returns {Slot}
   */
  setActiveItem: function (i) {
    if (!this.items.length) return this;
    (null === i) || (-1 !== _.indexOf(_.range(this.items.length), i, true)) ||
      (i = 0);
    if (i !== this.activeItem) {
      activate.call(this, i);
      this.activeItem = i;
    }
    return this;
  },

  /** @inheritDoc */
  animate: function (cls) {
    this.el && this.each(function (item) { item.animate && item.animate(cls) });
    return this;
  },

  listeners: {
    beforeRender: function () {
      this.items[this.activeItem] ? createItem.call(this,
        this.items[this.activeItem], this.activeItem, this.items) :
        this.each(createItem);
      updatePointers.call(this);
    },
    show: function () {
      this.each(function (item) { item.fire && item.fire('show') });
    },
    hide: function () {
      this.each(function (item) { item.fire && item.fire('hide') });
    },
    destroy: function () {
      if (this.el) {
        this.destroing = true;
        this.each(function (item) { item.destroy && item.destroy() });
      }
    }
  }

});

}());